C++

C++入门(三)

Posted by alonealice on 2021-02-24

继承

继承的一般语法为:

class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};

继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。

1) public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

2) protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

3) private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

基类成员在派生类中的访问权限不得高于继承方式中指定的权限。

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//基类People
class People{
public:
void show();
protected:
char *m_name;
int m_age;
};
void People::show(){
cout<<m_name<<"的年龄是"<<m_age<<endl;
}
//派生类Student
class Student: public People{
public:
void learning();
public:
using People::m_name; //将private改为public
using People::m_age; //将private改为public
float m_score;
private:
using People::show; //将public改为private
};

方法遮蔽

基类成员和派生类成员的名字一样时会造成遮蔽,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//基类Base
class Base{
public:
void func();
void func(int);
};
void Base::func(){ cout<<"Base::func()"<<endl; }
void Base::func(int a){ cout<<"Base::func(int)"<<endl; }
//派生类Derived
class Derived: public Base{
public:
void func(char *);
void func(bool);
};
void Derived::func(char *str){ cout<<"Derived::func(char *)"<<endl; }
void Derived::func(bool is){ cout<<"Derived::func(bool)"<<endl; }
int main(){
Derived d;
d.func("c.biancheng.net");
d.func(true);
d.func(); //compile error
d.func(10); //compile error
d.Base::func();
d.Base::func(100);
return 0;
}

构造方法和析构方法

在派生类的构造方法中调用基类的构造方法:

1
2
3
4
5
6
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ } //顺序可以交换
相当于
Student::Student(char *name, int age, float score){
People(name, age);
m_score = score;
}

析构函数的执行顺序和构造函数的执行顺序刚好相反。

多继承

多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

1
2
3
class D: public A, private B, protected C{
//类D新增加的成员
}

上面的 A、B、C、D 类为例,D 类构造函数的写法为:

1
2
3
D(形参列表): A(实参列表), B(实参列表), C(实参列表){
//其他操作
}

当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

1
2
BaseA::show();  //调用BaseA类的show()函数
BaseB::show(); //调用BaseB类的show()函数

虚继承和虚基类

在继承方式前面加上 virtual 关键字就是虚继承

1
2
3
4
5
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

向上转型(将派生类赋值给基类)

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题

可以用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。赋值后方法调用仍然是原来的。

1
2
3
4
5
6
7
8
9
A a(10);
B b(66, 99);
//赋值前
a.display(); // a
b.display(); //b
//赋值后
a = b;
a.display(); //a
b.display(); //b

除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针

1
2
3
4
5
6
7
A *pa = new A(1);
B *pb = new B(2, 20);
C *pc = new C(3);
D *pd = new D(4, 40, 400, 4000);
pa = pd;
pb = pd;
pc = pd;

虚函数

虚函数对于多态具有决定性的作用,有虚函数才能构成多态。

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。

  2. 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。

  3. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。

  4. 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)

  5. 构造函数不能是虚函数。

  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//基类Base
class Base{
public:
virtual void func();
virtual void func(int);
};
void Base::func(){
cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
cout<<"void Base::func(int)"<<endl;
}
//派生类Derived
class Derived: public Base{
public:
void func();
void func(char *);
};
void Derived::func(){
cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
cout<<"void Derived::func(char *)"<<endl;
}
int main(){
Base *p = new Derived();
p -> func(); //输出void Derived::func()
p -> func(10); //输出void Base::func(int)
p -> func("http://c.biancheng.net"); //compile error
return 0;
}

基类的方便不会被覆盖了

纯虚函数和抽象类

在C++中,可以将虚函数声明为纯虚函数,语法格式为:

virtual 返回值类型 函数名 (函数参数) = 0;

纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。它只起形式上的作用,告诉编译系统“这是纯虚函数”。

包含纯虚函数的类称为抽象类。

typeid运算符

typeid 运算符用来获取一个表达式的类型信息。类型信息对于编程语言非常重要,它描述了数据的各种属性:

  • 对于基本类型(int、float 等C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。
  • 对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。

typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:

typeid( dataType )
typeid( expression )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base{ };
struct STU{ };
int main(){
//获取一个普通变量的类型信息
int n = 100;
const type_info &nInfo = typeid(n);
// int | .H | 529034928
cout<<nInfo.name()<<" | "<<nInfo.raw_name()<<" | "<<nInfo.hash_code()<<endl;
//获取一个字面量的类型信息
// double | .N | 667332678
const type_info &dInfo = typeid(25.65);
cout<<dInfo.name()<<" | "<<dInfo.raw_name()<<" | "<<dInfo.hash_code()<<endl;
//获取一个对象的类型信息
// class Base | .?AVBase@@ | 1035034353
Base obj;
const type_info &objInfo = typeid(obj);
cout<<objInfo.name()<<" | "<<objInfo.raw_name()<<" | "<<objInfo.hash_code()<<endl;
//获取一个类的类型信息
// class Base | .?AVBase@@ | 1035034353
const type_info &baseInfo = typeid(Base);
cout<<baseInfo.name()<<" | "<<baseInfo.raw_name()<<" | "<<baseInfo.hash_code()<<endl;
//获取一个结构体的类型信息
// struct STU | .?AUSTU@@ | 734635517
const type_info &stuInfo = typeid(struct STU);
cout<<stuInfo.name()<<" | "<<stuInfo.raw_name()<<" | "<<stuInfo.hash_code()<<endl;
//获取一个普通类型的类型信息
// char | .D | 4140304029
const type_info &charInfo = typeid(char);
cout<<charInfo.name()<<" | "<<charInfo.raw_name()<<" | "<<charInfo.hash_code()<<endl;
//获取一个表达式的类型信息
// double | .N | 667332678
const type_info &expInfo = typeid(20 * 45 / 4.5);
cout<<expInfo.name()<<" | "<<expInfo.raw_name()<<" | "<<expInfo.hash_code()<<endl;
return 0;
}

type_info 类的几个成员函数,下面是对它们的介绍:

  • name() 用来返回类型的名称。
  • raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称。关于名字编码的概念,我们已在《C++函数编译原理和成员函数的实现》中讲到。
  • hash_code() 用来返回当前类型对应的 hash 值

typeid 运算符经常被用来判断两个类型是否相等

1
2
3
4
char *str;
int a = 2;
int b = 10;
float f;
类型比较 结果 类型比较 结果
typeid(int) == typeid(int) true typeid(int) == typeid(char) false
typeid(char*) == typeid(char) false typeid(str) == typeid(char*) true
typeid(a) == typeid(int) true typeid(b) == typeid(int) true
typeid(a) == typeid(a) true typeid(a) == typeid(b) true
typeid(a) == typeid(f) false typeid(a/b) == typeid(int) true
1
2
3
4
5
class Base{};
class Derived:
public Base{};
Base obj1;Base *p1;
Derived obj2;Derived *p2 = new Derived;p1 = p2;

类型判断结果为:

类型比较 结果 类型比较 结果
typeid(obj1) == typeid(p1) false typeid(obj1) == typeid(*p1) true
typeid(&obj1) == typeid(p1) true typeid(obj1) == typeid(obj2) false
typeid(obj1) == typeid(Base) true typeid(*p1) == typeid(Base) true
typeid(p1) == typeid(Base*) true typeid(p1) == typeid(Derived*) false

运算符重载

运算符重载是通过函数实现的,它本质上是函数重载。

运算符重载的格式为:

返回值类型 operator 运算符名称 (形参表列){
//TODO:
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class complex{
public:
complex();
complex(double real, double imag);
public:
//声明运算符重载
complex operator+(const complex &A) const;
void display() const;
private:
double m_real; //实部
double m_imag; //虚部
};
complex::complex(): m_real(0.0), m_imag(0.0){ }
complex::complex(double real, double imag): m_real(real), m_imag(imag){ }
//实现运算符重载
complex complex::operator+(const complex &A) const{
complex B;
B.m_real = this->m_real + A.m_real;
B.m_imag = this->m_imag + A.m_imag;
return B;
}
void complex::display() const{
cout<<m_real<<" + "<<m_imag<<"i"<<endl;
}
int main(){
complex c1(4.3, 5.8);
complex c2(2.4, 3.7);
complex c3;
c3 = c1 + c2;
c3.display();
// 6.7 + 9.5i
return 0;
}

在全局范围内重载运算符

在上面基础上

1
2
3
4
5
6
7
8
9
10
 //声明为友元函数
friend complex operator+(const complex &A, const complex &B);

//在全局范围内重载+
complex operator+(const complex &A, const complex &B){
complex C;
C.m_real = A.m_real + B.m_real;
C.m_imag = A.m_imag + B.m_imag;
return C;
}

运算符重载的规则

  1. 并不是所有的运算符都可以重载。能够重载的运算符包括:
    + - * / % ^ & | ~ ! = < > += -= = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ – , -> -> () [] new new[] delete delete[]

长度运算符sizeof、条件运算符: ?、成员选择符.和域解析运算符::不能被重载。

  1. 重载不能改变运算符的优先级和结合性。假设上一节的 complex 类中重载了+号和*号,并且 c1、c2、c3、c4 都是 complex 类的对象,那么下面的语句:

c4 = c1 + c2 * c3;

等价于:

c4 = c1 + ( c2 * c3 );

  1. 重载不会改变运算符的用法,原有有几个操作数、操作数在左边还是在右边,这些都不会改变。

  2. 运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数,这显然是错误的。

  3. 运算符重载函数既可以作为类的成员函数,也可以作为全局函数。

  4. 箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。

函数模板

所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。

定义模板函数的语法:

1
2
3
template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型  函数名(形参列表){
//在函数体中可以使用类型参数
}

typename关键字也可以使用class关键字替代,它们没有任何区别。

举例:

1
2
3
4
5
6
7
8
9
10
11
template<typename T> void Swap(T *a, T *b){
T temp = *a;
*a = *b;
*b = temp;
}

template<typename T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}

函数模板也可以提前声明,不过声明时需要带上模板头,并且模板头和函数定义(声明)是一个不可分割的整体,它们可以换行,但中间不能有分号。

1
2
3
4
5
6
7
8
9
10
11
//声明函数模板
template<typename T> T max(T a, T b, T c);

//定义函数模板
template<typename T> //模板头,这里不能有分号
T max(T a, T b, T c){ //函数头
T max_num = a;
if(b > max_num) max_num = b;
if(c > max_num) max_num = c;
return max_num;
}

类模板

声明类模板的语法为:

1
2
3
template<typename 类型参数1 , typename 类型参数2 , …> class 类名{
//TODO:
};
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T1, typename T2>  //这里不能有分号
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const; //获取x坐标
void setX(T1 x); //设置x坐标
T2 getY() const; //获取y坐标
void setY(T2 y); //设置y坐标
private:
T1 m_x; //x坐标
T2 m_y; //y坐标
};